package vulnerability

import (
	"log"
	"sort"
	"strings"

	bolt "go.etcd.io/bbolt"
	"golang.org/x/xerrors"

	"github.com/aquasecurity/trivy-db/pkg/db"
	"github.com/aquasecurity/trivy-db/pkg/types"
)

const (
	rejectVulnerability = "** REJECT **"
)

var (
	sources = []string{NVD, RedHat, Debian, DebianOVAL, Ubuntu, Alpine, Amazon, OracleOVAL, SuseCVRF, Photon,
		RubySec, RustSec, PhpSecurityAdvisories, NodejsSecurityWg, PythonSafetyDB,
		GHSAComposer, GHSAMaven, GHSANpm, GHSANuget, GHSAPip, GHSARubygems, GLAD,
	}
)

type Vulnerability struct {
	dbc db.Operation
}

func New(dbc db.Operation) Vulnerability {
	return Vulnerability{dbc: dbc}
}

func (v Vulnerability) GetDetails(vulnID string) map[string]types.VulnerabilityDetail {
	details, err := v.dbc.GetVulnerabilityDetail(vulnID)
	if err != nil {
		log.Println(err)
		return nil
	} else if len(details) == 0 {
		return nil
	}
	return details
}

func (Vulnerability) IsRejected(details map[string]types.VulnerabilityDetail) bool {
	return getRejectedStatus(details)
}

func (Vulnerability) Normalize(details map[string]types.VulnerabilityDetail) types.Vulnerability {
	return types.Vulnerability{
		Title:            getTitle(details),
		Description:      getDescription(details),
		Severity:         getSeverity(details).String(), // TODO: We have to keep this key until we deprecate
		CweIDs:           getCweIDs(details),
		VendorSeverity:   getVendorSeverity(details),
		CVSS:             getCVSS(details),
		References:       getReferences(details),
		PublishedDate:    details[NVD].PublishedDate,
		LastModifiedDate: details[NVD].LastModifiedDate,
	}
}

func (v Vulnerability) SaveAdvisoryDetails(tx *bolt.Tx, cveID string) error {
	// extract advisories from 'advisory-detail' bucket
	advisories, err := v.dbc.GetAdvisoryDetails(cveID)
	if err != nil {
		return xerrors.Errorf("failed to get advisories: %w", err)
	}

	// put advisories into 'advisory' bucket
	for _, advisory := range advisories {
		if err := v.dbc.PutAdvisory(tx, advisory.PlatformName, advisory.PackageName, cveID, advisory.AdvisoryItem); err != nil {
			return xerrors.Errorf("failed to save %v advisory: %w", advisory.PlatformName, err)
		}
	}
	return nil
}

func getCVSS(details map[string]types.VulnerabilityDetail) types.VendorCVSS {
	vc := make(types.VendorCVSS)
	for vendor, detail := range details {
		if (detail.CvssVector == "" || detail.CvssScore == 0) && (detail.CvssVectorV3 == "" || detail.CvssScoreV3 == 0) {
			continue
		}
		vc[vendor] = types.CVSS{
			V2Vector: detail.CvssVector,
			V3Vector: detail.CvssVectorV3,
			V2Score:  detail.CvssScore,
			V3Score:  detail.CvssScoreV3,
		}
	}
	return vc
}

func getVendorSeverity(details map[string]types.VulnerabilityDetail) types.VendorSeverity {
	vs := make(types.VendorSeverity)
	for vendor, detail := range details {
		switch {
		case detail.SeverityV3 != types.SeverityUnknown:
			vs[vendor] = detail.SeverityV3
		case detail.Severity != types.SeverityUnknown:
			vs[vendor] = detail.Severity
		case detail.CvssScoreV3 > 0:
			vs[vendor] = scoreToSeverity(detail.CvssScoreV3)
		case detail.CvssScore > 0:
			vs[vendor] = scoreToSeverity(detail.CvssScore)
		}
	}
	return vs
}

func getSeverity(details map[string]types.VulnerabilityDetail) types.Severity {
	for _, source := range sources {
		switch d, ok := details[source]; {
		case !ok:
			continue
		case d.CvssScoreV3 > 0:
			return scoreToSeverity(d.CvssScoreV3)
		case d.CvssScore > 0:
			return scoreToSeverity(d.CvssScore)
		case d.SeverityV3 != 0:
			return d.SeverityV3
		case d.Severity != 0:
			return d.Severity
		}
	}
	return types.SeverityUnknown
}

func getTitle(details map[string]types.VulnerabilityDetail) string {
	for _, source := range sources {
		d, ok := details[source]
		if !ok {
			continue
		}
		if d.Title != "" {
			return d.Title
		}
	}
	return ""
}

func getDescription(details map[string]types.VulnerabilityDetail) string {
	for _, source := range sources {
		d, ok := details[source]
		if !ok {
			continue
		}
		if d.Description != "" {
			return d.Description
		}
	}
	return ""
}

func getCweIDs(details map[string]types.VulnerabilityDetail) []string {
	for _, source := range sources {
		d, ok := details[source]
		if !ok {
			continue
		}
		if len(d.CweIDs) != 0 {
			return d.CweIDs
		}
	}
	return nil
}

func getReferences(details map[string]types.VulnerabilityDetail) []string {
	references := map[string]struct{}{}
	for _, source := range sources {
		// Amazon contains unrelated references
		if source == Amazon {
			continue
		}
		d, ok := details[source]
		if !ok {
			continue
		}
		for _, ref := range d.References {
			// e.g. "\nhttps://curl.haxx.se/docs/CVE-2019-5481.html\n    "
			ref = strings.TrimSpace(ref)
			for _, r := range strings.Split(ref, "\n") {
				references[r] = struct{}{}
			}
		}
	}
	var refs []string
	for ref := range references {
		refs = append(refs, ref)
	}
	sort.Slice(refs, func(i, j int) bool {
		return refs[i] < refs[j]
	})
	return refs
}

func getRejectedStatus(details map[string]types.VulnerabilityDetail) bool {
	for _, source := range sources {
		d, ok := details[source]
		if !ok {
			continue
		}
		if strings.Contains(d.Description, rejectVulnerability) {
			return true
		}
	}
	return false
}

func scoreToSeverity(score float64) types.Severity {
	switch {
	case score >= 9.0:
		return types.SeverityCritical
	case score >= 7.0:
		return types.SeverityHigh
	case score >= 4.0:
		return types.SeverityMedium
	case score > 0.0:
		return types.SeverityLow
	default:
		return types.SeverityUnknown
	}
}
